package it.tukano.treditor.engine.jme3;

import com.jme3.app.SimpleApplication;
import com.jme3.asset.AssetManager;
import com.jme3.asset.plugins.UrlLocator;
import com.jme3.bounding.BoundingBox;
import com.jme3.bounding.BoundingSphere;
import com.jme3.bounding.BoundingVolume;
import com.jme3.collision.CollisionResult;
import com.jme3.collision.CollisionResults;
import com.jme3.light.DirectionalLight;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Quaternion;
import com.jme3.math.Ray;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.VertexBuffer.Type;
import com.jme3.scene.plugins.OBJLoader;
import com.jme3.system.AppSettings;
import com.jme3.system.JmeCanvasContext;
import com.jme3.texture.plugins.AWTLoader;
import com.jme3.util.BufferUtils;
import it.tukano.check.Checks;
import it.tukano.check.Todo;
import it.tukano.log.Log;
import it.tukano.treditor.engine.Engine3D;
import it.tukano.treditor.engine.EngineQueryInterface;
import it.tukano.treditor.engine.geom.EulerSet;
import it.tukano.treditor.engine.geom.NSegment;
import it.tukano.treditor.engine.geom.NTriangle;
import it.tukano.treditor.engine.geom.NTuple2;
import it.tukano.treditor.engine.geom.NTuple3;
import it.tukano.treditor.engine.input.EngineInputListener;
import it.tukano.treditor.engine.mesh.Mesh3D;
import it.tukano.treditor.engine.scene.DirectionalLightElement;
import it.tukano.treditor.engine.scene.MeshElement;
import it.tukano.treditor.engine.scene.ModelElement;
import it.tukano.treditor.engine.scene.NodeElement;
import it.tukano.treditor.engine.scene.SceneElement;
import it.tukano.treditor.engine.scene.SceneView;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.net.URL;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import javax.swing.JComponent;
import javax.swing.JPanel;

/**
 * JME3 based engine for the 3dtor program
 * @author pgi
 */
public class JME3Engine implements Engine3D {

    private final SimpleApplication APPLICATION = new SimpleApplication() {

        @Override
        public void simpleInitApp() {
            assetManager.registerLocator("file:/", UrlLocator.class);
            assetManager.registerLoader(AWTLoader.class, "png");
            assetManager.registerLoader(OBJLoader.class, "obj");
            setDisplayFps(false);
            setDisplayStatView(false);
            flyCam.setEnabled(false);
            JME3Engine.this.assetManager = assetManager;
            sceneHook = new Node("Scene");
            tempHook = new Node("Temp");
            rootNode.attachChild(sceneHook);
            rootNode.attachChild(tempHook);
            inputManager.addRawInputListener(producer);
        }
    };
    private final ABMap<Spatial, SceneElement> sceneMap = new ABMap<>();
    private final Map<SceneElement, LightData> lightMappings = new HashMap<>();
    private Canvas canvas;
    /* Ogl thread fields */
    private Node sceneHook;
    private Node tempHook;
    private AssetManager assetManager;
    private final JPanel COMPONENT = new JPanel(new GridLayout(1, 1));
    private final AWTInputProducer producer = new AWTInputProducer(COMPONENT, APPLICATION);
    
    /**
     * Instance initializer
     */
    public JME3Engine() {
        COMPONENT.addComponentListener(new ComponentAdapter() {

            @Override
            public void componentShown(ComponentEvent e) {
                APPLICATION.startCanvas();
            }
        });
    }

    private void generateJmeBranch(Node jmeParent, NodeElement treditorBranch) {
        Node node = new Node(treditorBranch.getName());
        jmeParent.attachChild(node);
        sceneMap.put(node, treditorBranch);

        for (int i = 0; i < treditorBranch.getChildrenCount(); i++) {
            SceneElement child = treditorBranch.getChild(i);
            switch (child.getType()) {
                case NODE:
                    generateJmeBranch(node, (NodeElement) child);
                    break;
                case MODEL:
                    addModelToNode((ModelElement) child, treditorBranch);
                    break;
                case DIRECTIONAL_LIGHT:
                    addDirectionalLightToNode((DirectionalLightElement) child, treditorBranch);
                    break;
                case MESH:
                    addMeshToNode((MeshElement) child, treditorBranch);
                default:
                    Todo.implementThis("parse element: ", child.getType());
            }
        }
    }

    private void submit(Callable<?> call) {
        APPLICATION.enqueue(call);
    }
    
    /** 
     * Converts a tuple3n into a vector3f
     */
    private Vector3f tupleToVector(NTuple3 t) {
        return new Vector3f(t.getX().floatValue(), t.getY().floatValue(), t.getZ().floatValue());
    }
    
    /**
     * Converts a java.awt.Color into a ColorRGBA
     */
    private ColorRGBA colorToColorRGBA(Color c) {
        ColorRGBA color = new ColorRGBA();
        color.fromIntARGB(c.getRGB());
        return color;
    }

    /**
     * Transforms a euler set into a quaternion
     */
    private Quaternion eulerToQuat(EulerSet orientation) {
        orientation = orientation.toRadians();
        final Quaternion quat = new Quaternion().fromAngles(
                orientation.getX().floatValue(),
                orientation.getY().floatValue(),
                orientation.getZ().floatValue());
        return quat;
    }
    
    private void doRemoveElementFromNode(SceneElement element, NodeElement node) {
        switch(element.getType()) {
            case NODE:
                JME3Engine.this.oglRemoveNodeFromNode((NodeElement) element, node);
                break;
            case DIRECTIONAL_LIGHT:
                JME3Engine.this.oglRemoveDirectionalLightFromNode((DirectionalLightElement) element, node);
                break;
            case MODEL:
                JME3Engine.this.oglRemoveModelFromNode((ModelElement) element, node);
                break;
            case MESH:
                JME3Engine.this.oglRemoveMeshFromNode((MeshElement) element, node);
                break;
        }
    }

    private void oglRemoveNodeFromNode(NodeElement nodeElement, NodeElement parent) {
        System.out.println("remove node from node..." + nodeElement + "," + parent);
        System.out.println("Children count: " + nodeElement.getChildrenCount());
        Spatial removedNode = sceneMap.getA(nodeElement);
        sceneMap.removeB(nodeElement);
        removedNode.removeFromParent();
        for (int i = 0; i < nodeElement.getChildrenCount(); i++) {
            System.out.println("sub remove");
            SceneElement child = nodeElement.getChild(i);
            doRemoveElementFromNode(child, parent);
        }
    }

    private void oglRemoveDirectionalLightFromNode(DirectionalLightElement directionalLightElement, NodeElement parent) {
        LightData lightData = lightMappings.get(directionalLightElement);
        Node node = lightData.getNode();
        node.removeLight(lightData.getLight());
        lightMappings.remove(directionalLightElement);
    }

    private void oglRemoveModelFromNode(ModelElement modelElement, NodeElement parent) {
        Spatial removedModel = sceneMap.getA(modelElement);
        sceneMap.removeB(modelElement);
        removedModel.removeFromParent();
    }

    private void oglRemoveMeshFromNode(MeshElement meshElement, NodeElement node) {
        Spatial removedModel = sceneMap.getA(meshElement);
        sceneMap.removeB(meshElement);
        removedModel.removeFromParent();
        System.out.println("Removed:" + removedModel);
    }
    
    private SceneElement findMatchingSceneElement(Spatial jmeSpatial) {
        while(jmeSpatial != null) {
            SceneElement e = sceneMap.getB(jmeSpatial);
            if(e != null) return e;
            jmeSpatial = jmeSpatial.getParent();
        }
        return null;
    }

    private Spatial convertMeshToSpatial(MeshElement meshElement) {
        Mesh3D mesh = meshElement.getMesh();
        Mesh lines = convertMeshToJmeLines(mesh);
        Mesh triangles = convertMeshToJmeTriangles(mesh);
        Material material = convertMeshToJmeMaterial(meshElement);
        Node node = new Node("mesh");
        if(lines != null) {
            Geometry geom = new Geometry("mesh", lines);
            geom.setMaterial(material);
            geom.setModelBound(new BoundingSphere());
            node.attachChild(geom);
        }
        if(triangles != null) {
            Geometry geom = new Geometry("mesh", triangles);
            geom.setMaterial(material);
            geom.setModelBound(new BoundingSphere());
            node.attachChild(geom);
        }
        node.setLocalRotation(eulerToQuat(meshElement.getLocalRotation()));
        node.setLocalTranslation(tupleToVector(meshElement.getLocalTranslation()));
        node.setLocalScale(tupleToVector(meshElement.getLocalScale()));
        node.setModelBound(new BoundingBox());
        node.updateModelBound();
        return node;
    }

    private Mesh convertMeshToJmeLines(Mesh3D mesh) {
        int max = mesh.getSegmentCount();
        if(max == 0) return null;
        
        FloatBuffer coordinates = BufferUtils.createFloatBuffer(max * 2 * 3);
        FloatBuffer normals = BufferUtils.createFloatBuffer(max * 2 * 3);
        IntBuffer indices = BufferUtils.createIntBuffer(max * 2);
        for(int i = 0, index = 0; i < max; i++, index+=2) {
            NSegment segment = mesh.getSegment(i);
            transferFloat(coordinates, segment.getStart(), segment.getEnd());
            normals.put(0).put(1).put(0);
            normals.put(0).put(1).put(0);
            indices.put(index).put(index + 1);
        }
        coordinates.flip();
        normals.flip();
        indices.flip();
        
        Mesh m = new Mesh();
        m.setMode(Mesh.Mode.Lines);
        m.setBuffer(Type.Position, 3, coordinates);
        m.setBuffer(Type.Normal, 3, normals);
        m.setBuffer(Type.Index, 2, indices);
        return m;
    }

    private void transferFloat(FloatBuffer buffer, NTuple3... tuples) {
        for(int i = 0; i < tuples.length; i++) {
            NTuple3 tuple = tuples[i];
            buffer.put(tuple.getX().floatValue()).put(tuple.getY().floatValue()).put(tuple.getZ().floatValue());
        }
    }
    
    private void transferFloat(FloatBuffer buffer, NTuple2... tuples) {
        for(int i = 0; i < tuples.length; i++) {
            NTuple2 tuple = tuples[i];
            buffer.put(tuple.getX().floatValue()).put(tuple.getY().floatValue());
        }
    }
    
    private Mesh convertMeshToJmeTriangles(Mesh3D mesh) {
        int triangleCount = mesh.getTriangleCount();
        if(triangleCount == 0) return null;
        
        FloatBuffer coords = BufferUtils.createFloatBuffer(triangleCount * 3 * 3);
        FloatBuffer normals = BufferUtils.createFloatBuffer(triangleCount * 3 * 3);
        FloatBuffer texc = BufferUtils.createFloatBuffer(triangleCount * 3 * 2);
        IntBuffer indices = BufferUtils.createIntBuffer(triangleCount * 3);
        
        boolean
                hasCoords = true,
                hasNormals = true,
                hasTexCoords = true;
        
        for(int i = 0, index = 0; i < triangleCount; i++, index += 3) {
            NTriangle triangle = mesh.getTriangle(i);
            if (triangle.hasCoords()) {
                transferFloat(coords, triangle.getV0(), triangle.getV1(), triangle.getV2());
            } else {
                hasCoords = false;
            }
            if (triangle.hasNormals()) {
                transferFloat(normals, triangle.getN0(), triangle.getN1(), triangle.getN2());
            } else {
                hasNormals = false;
            }
            if (triangle.hasTexCoords()) {
                transferFloat(texc, triangle.getT0(), triangle.getT1(), triangle.getT2());
            } else {
                hasTexCoords = false;
            }
            indices.put(index).put(index + 1).put(index + 2);
        }
        
        coords.flip();
        normals.flip();
        texc.flip();
        indices.flip();
        
        Mesh m = new Mesh();
        m.setMode(Mesh.Mode.Triangles);
        if(hasCoords) m.setBuffer(Type.Position, 3, coords);
        if(hasNormals) m.setBuffer(Type.Normal, 3, normals);
        if(hasTexCoords) m.setBuffer(Type.TexCoord, 2, texc);
        return m;
    }

    private Material convertMeshToJmeMaterial(MeshElement meshElement) {
        Mesh3D mesh = meshElement.getMesh();
        Material mat;
        if(mesh.hasColor()) {
            mat = new Material(assetManager, "Common/MatDefs/Misc/SolidColor.j3md");
            mat.setColor("Color", tupleToColor(mesh.getColor()));
        } else {
            mat = new Material(assetManager, "Common/MatDefs/Misc/SolidColor.j3md");
        }
        return mat;
    }

    private ColorRGBA tupleToColor(NTuple3 color) {
        return new ColorRGBA(color.getX().floatValue(), color.getY().floatValue(), color.getZ().floatValue(), 1f);
    }
    
    private void removeSceneElementsFromSceneMap() {
        List<Spatial> spatials = sceneMap.listA();
        for (Spatial spatial : spatials) {
            if(isSceneElement(spatial)) {
                sceneMap.removeA(spatial);
            }
        }
    }
    
    private boolean isSceneElement(Spatial spatial) {
        Spatial parent = spatial;
        while(parent != null) {
            if(parent == sceneHook) return true;
            parent = parent.getParent();
        }
        return false;
    }

/* Engine interface */    
    
    /**
     * Checks if the model is in a supported format
     * @param modelAddress the url of the model
     * @return true if the model is in obj format (temp)
     */
    @Override
    public boolean isModelFormatSupported(URL modelAddress) {
        Checks.NotNull(modelAddress);
        final String path = modelAddress.toString().toLowerCase();
        return path.endsWith(".obj")
                || path.endsWith(".blend")
                || path.endsWith(".mesh.xml")
                || path.endsWith(".scene");
    }

    @Override
    public void edtInitialize() {
        AppSettings settings = new AppSettings(true);
        settings.setWidth(640);
        settings.setHeight(480);
        APPLICATION.setPauseOnLostFocus(false);
        APPLICATION.setSettings(settings);
        APPLICATION.createCanvas();
        JmeCanvasContext context = (JmeCanvasContext) APPLICATION.getContext();
        canvas = context.getCanvas();
        canvas.setSize(640, 480);
        canvas.setPreferredSize(new Dimension(640, 480));
        canvas.setMinimumSize(new Dimension(640, 480));
        COMPONENT.add(canvas);
    }

    @Override
    public JComponent edtGetDisplayComponent() {
        return COMPONENT;
    }

    @Override
    public void edtDispose() {
        new Thread() {

            @Override public void run() {
                APPLICATION.stop(true);
                APPLICATION.destroy();
            }
        }.start();
    }

    @Override
    public void setSceneRoot(final NodeElement newRoot) {
        submit(new VoidCall() {

            @Override protected void exec() {
                sceneHook.detachAllChildren();
                removeSceneElementsFromSceneMap();
                lightMappings.clear();
                generateJmeBranch(sceneHook, newRoot);
            }
        });
    }

    @Override
    public void setTempRoot(final NodeElement newTempRoot) {
        submit(new VoidCall() {

            @Override
            void exec() {
                tempHook.detachAllChildren();
                generateJmeBranch(tempHook, newTempRoot);
            }
        });
    }

    @Override 
    public void addModelToNode(final ModelElement modelElement, final NodeElement nodeElement) {
        submit(new VoidCall() {

            @Override
            void exec() {
                final URL modelFileAddress = modelElement.getModelFileAddress();
                final String base = modelFileAddress.toString().substring(0, modelFileAddress.toString().lastIndexOf("/"));
                final Spatial modelSpatial = loadModel(modelFileAddress);
                final Spatial parentSpatial = sceneMap.getA(nodeElement);
                final boolean modelFound = modelSpatial != null;
                final boolean parentIsNode = parentSpatial instanceof Node;
                final NTuple3 localScale = modelElement.getLocalScale();
                final EulerSet localRotation = modelElement.getLocalRotation().toRadians();
                final NTuple3 localTranslation = modelElement.getLocalTranslation();

                if (modelFound && parentIsNode) {
                    final Node parentNode = (Node) parentSpatial;
                    modelSpatial.setLocalRotation(eulerToQuat(localRotation));
                    modelSpatial.setLocalScale(tupleToVector(localScale));
                    modelSpatial.setLocalTranslation(tupleToVector(localTranslation));
                    parentNode.attachChild(modelSpatial);
                    sceneMap.put(modelSpatial, modelElement);
                } else if (!modelFound) {
                    Todo.implementThis("cant' load model");
                } else if (!parentIsNode) {
                    Todo.implementThis("parent is not a node: ", parentSpatial);
                }
            }
        });
    }
    
    @Override
    public void addMeshToNode(final MeshElement mesh, final NodeElement node) {
        submit(new VoidCall() {

            @Override
            void exec() {
                final Spatial spatial = JME3Engine.this.convertMeshToSpatial(mesh);
                final Spatial parent = sceneMap.getA(node);
                if(spatial != null && (parent instanceof Node)) {
                    final Node parentNode = (Node) parent;
                    parentNode.attachChild(spatial);
                    sceneMap.put(spatial, mesh);
                }
            }
        });
    }

    @Override 
    public void addEmptyNodeToNode(final NodeElement emptyChild, final NodeElement parentElement) {
        submit(new VoidCall() {

            void exec() {
                final Spatial parentNode = sceneMap.getA(parentElement);
                final boolean parentNodeIsNode = parentNode instanceof Node;
                final Node newChildNode = new Node(emptyChild.getName());
                sceneMap.put(newChildNode, emptyChild);
                if (parentNodeIsNode) {
                    final Node parent = (Node) parentNode;
                    parent.attachChild(newChildNode);
                } else {
                    Todo.implementThis();
                }
            }
        });
    }

    private Spatial loadModel(URL modelFileAddress) {
        Log.message("Loading:", modelFileAddress);
        Spatial model = null;
        try {
            model = assetManager.loadModel(modelFileAddress.toString());
        } catch (Exception ex) {
            Todo.implementThis("cannot load model due to an exception", ex);
        }
        return model;
    }

    @Override
    public void addDirectionalLightToNode(final DirectionalLightElement lightElement, final NodeElement parentElement) {
        submit(new VoidCall() {

            void exec() {
                final Spatial parentSpatial = sceneMap.getA(parentElement);
                final boolean parentSpatialIsNode = parentSpatial instanceof Node;
                final NTuple3 direction = lightElement.getDirection();
                final Color color = lightElement.getColor();
                if (parentSpatialIsNode) {
                    final Node parentNode = (Node) parentSpatial;
                    final ColorRGBA lightColor = new ColorRGBA();
                    lightColor.fromIntARGB(color.getRGB());
                    final DirectionalLight light = new DirectionalLight();
                    final Vector3f lightDirection = tupleToVector(direction);

                    light.setColor(lightColor);
                    light.setDirection(lightDirection);
                    parentNode.addLight(light);
                    LightData ld = new LightData(light, parentNode);
                    lightMappings.put(lightElement, ld);
                } else {
                    Todo.implementThis();
                }
            }
        });
    }

    @Override
    public void setView(final SceneView view) {
        submit(new VoidCall() {

            void exec() {
                final NTuple3 location = view.getLocation();
                final EulerSet orientation = view.getOrientation().toRadians();
                final Quaternion quat = eulerToQuat(orientation);
                final Vector3f vec = tupleToVector(location);
                final Camera camera = APPLICATION.getCamera();
                camera.setLocation(vec);
                camera.setAxes(quat);
            }
        });
    }

    @Override
    public String[] getSupportedModelFormatExtensions() {
        return new String[]{"obj", "mesh.xml", "blend"};
    }

    @Override
    public void updateModelTransform(final ModelElement updatedModel) {
        submit(new VoidCall() {

            @Override
            void exec() {
                final Vector3f scale = tupleToVector(updatedModel.getLocalScale());
                final Vector3f translation = tupleToVector(updatedModel.getLocalTranslation());
                final Quaternion rotation = eulerToQuat(updatedModel.getLocalRotation());
                final Spatial spatial = sceneMap.getA(updatedModel);
                if(spatial != null) {
                    spatial.setLocalRotation(rotation);
                    spatial.setLocalScale(scale);
                    spatial.setLocalTranslation(translation);
                } else {
                    Todo.implementThis();
                }
            }
        });
    }

    @Override
    public void updateDirectionalLight(final DirectionalLightElement updatedLight) {
        submit(new VoidCall() {

            @Override
            void exec() {
                LightData mappedLight = lightMappings.get(updatedLight);
                Checks.NotNull(mappedLight);
                DirectionalLight light = (DirectionalLight) mappedLight.getLight();
                Vector3f newDirection = tupleToVector(updatedLight.getDirection());
                ColorRGBA newColor = colorToColorRGBA(updatedLight.getColor());
                light.setColor(newColor);
                light.setDirection(newDirection);
            }
        });
    }

    @Override
    public void setEngineInputListener(final EngineInputListener listener) {
        submit(new VoidCall() {

            @Override
            void exec() {
                APPLICATION.getInputManager().addRawInputListener(new EngineInputHandler(listener, APPLICATION.getCamera()));
            }
        });
    }

    @Override
    public void removeElementFromNode(final SceneElement element, final NodeElement node) {
        submit(new VoidCall() {

            @Override
            void exec() {
                doRemoveElementFromNode(element, node);
            }
        });
    }

/* End of engine interface */    
    
/* Query interface section */
    
    @Override
    public EngineQueryInterface getQueryInterface() {
        return queryInterface;
    }
    
    private final EngineQueryInterface queryInterface = new EngineQueryInterface() {

        @Override
        protected void submitRunnableForExecutionInEngineThread(final Runnable task) {
            submit(new Callable<Void>() {

                @Override
                public Void call() throws Exception {
                    task.run();
                    return null;
                }
            });
        }

        @Override
        protected it.tukano.treditor.engine.geom.BoundingSphere computeGlobalBoundingSphere(SceneElement e) {
            Spatial spatial = sceneMap.getA(e);
            BoundingVolume worldBound = spatial.getWorldBound();
            BoundingSphere sphere = new BoundingSphere();
            sphere.setCenter(worldBound.getCenter());
            sphere.mergeLocal(worldBound);
            Vector3f center = sphere.getCenter();
            float radius = sphere.getRadius();
            return new it.tukano.treditor.engine.geom.BoundingSphere(radius, new NTuple3(center.x, center.y, center.z));
        }

        @Override
        protected SceneElement pickElementAtScreenPoint(SceneElement branch, NTuple2 screenPoint) {
            Spatial jmeBranch = sceneMap.getA(branch);
            if(jmeBranch == null) {
                Log.message("Cannot find element matching: ", branch);
                return null;
            } else {
                return pick(jmeBranch, screenPoint);
            }
        }
        
        private SceneElement pick(Spatial branch, NTuple2 screenPoint) {
            Vector2f screenPosition = new Vector2f(screenPoint.getX().floatValue(), COMPONENT.getHeight() - screenPoint.getY().floatValue());
            Camera camera = APPLICATION.getCamera();
            Vector3f location = camera.getLocation();
            Vector3f worldCoordinates = camera.getWorldCoordinates(screenPosition, 1);
            Vector3f direction = worldCoordinates.subtract(location).normalizeLocal();
            Ray ray = new Ray(location, direction);
            CollisionResults pickResults = new CollisionResults();
            branch.collideWith(ray, pickResults);
            CollisionResult closestCollision = pickResults.getClosestCollision();
            if(closestCollision == null) {
                Log.message("No collision at:", screenPoint);
                return null;
            }
            Geometry geometry = closestCollision.getGeometry();
            SceneElement element = findMatchingSceneElement(geometry);
            return element;
        }
    };
    
/* End of query interface section */
    
}
